May 2025 is the conclusion of the initial effort to bring support for Rust to Zephyr. At this point, the following are supported:
#zephyrthread can decorate a top level function (with
Send+Sized arguments) to make a function that instead creates a Zephyr thread and returns a handle
that can be used to start the thread and manage it.log crate will be send either to printk, or to Zephyr's log
mechanism.Duration and Time types. These can be used in most API calls
that expect a timeout.zephyr::kconfig::* and boolean
values can be made visible with a simple call in build.rs.zephyr::devicetree with a
module structure mirroring the devicetree. Presence conditional checks for nodes can be made
visible via a call in build.rs.wait_for_high/low async call.At any point that there is upstream support within Zephyr, there is a need to maintain this code. This involves:
In addition to the maintenance work, there are likely to be new features added to Zephyr that will require additional work to support.
Estimate: 10-20% of an engineer for maintenance.
New feature support is difficult to estimate.
With the work that is done, it is possible to write multi-thread or async programs on Zephyr. However, Zephyr contains significantly more functionality, in terms of both a multitude of drivers as well as a large number of subsystems.
The additional work can be placed into two categories: Flushing out the implemented functionality to be more complete, and possibly more usable from Rust, and adding support for drivers and subsystems.
Without additional work, it is possible for user to make use of other parts of Zephyr, however this approach misses out on many of the benefits of using Rust. To do this requires:
At some point, an effort should be made to stabilizes the interfaces. This should wait until the Rust/Zephyr interfaces have had some noticeable use, giving an opportunity to go through improvements without needing more involved deprecation processes.
Rust, unlike Zephyr, uses semantic versions. The current version is marked "0.1.0", indicating that it is unstable, and API changes can be expected. Once "1.0.0" is released, any changes to the API will need to undergo a deprecation process, and even compatible changes will need to track changes according to the rules of semantic versioning.
This should be undertaken after a majority of the work is finished.
Estimate: 4 weeks
The following tasks around the existing functionality will make the experience of using them more complete.
The Zephyr interfaces are primarily documented through the "rustdoc" mechanism, where the documentation is in comments. There may be some value in creating documentation covering broader aspects, such as how to work with a particular subsystem, and such.
Estimate: 2 weeks
Currently, in order to add bindings, a user must modify a few files within the rust-lang-zephyr repo, specifically within the zephyr-sys crate. These include:
wrapper.h - Add include directives for new apis. In addition, some #define values that are
expressions have to be explicitly declared via consts for bindgen to know they have constant
values and to be made visible. Occasionally, static functions might also have to be written.build.rs - There is a list of patterns of symbols that bindgen uses. Depending on the names of
the API, this list may need to be extended.It is unclear how this can be generalized by a user of the crate, and it probably makes sense to be fairly free in accepting contributions that add patterns here. The cost of additional patterns in slowing down the compilation slightly as the generated bindings will be larger. Unlike C, however, this is only done once per build, not for each file that uses the bindings.
Estimate: 0 weeks
There is an inherent semantic mismatch between how logging works in Rust vs Zephyr, mostly a consequence of the difference in how formatting strings work. Zephyr tries to delay formatting of log messages, but because C formatting typically only works on types that fit in registers, there aren't lifetime concerns with these values. Strings are handled as a special case in Zephyr, and the contents of the string are copied into the log buffer.
Rust string formatting, on the other hand, is much richer, allowing constructs such as "{:?}" to
format an arbitrary object using the Debug trait. Combined with the compiler support to
automatically derive implementations of this trait, this is a powerful tool when debugging. The
compile is able to generate these automatic derivations, and the code only ends up in the final
image when actually used in a message.
In the rust-embedded world, another crate, defmt is commonly
used for logging. This takes a very different approach. The static parts of strings for messages
are not included in the image (they are placed in a linker section that is included in the ELF file,
but not the on-target image), and the log messages are encoded in a compact format. There is
host-side tooling to decode the messages, by reading the message stream, and finding the strings in
the ELF file.
From discussions with the defmt developers: they recommend something like the following approach:
defmt-print tool to allow it to read both streams.Trying to match the log crate formatting better to Zephyr's log mechanism is also possible. In
general, this will likely always result in mostly pre-formatting the messages. It might also be
possible to decode format strings at compile time, and convert some cases of format strings to a
printf equivalent, allowing them to better co-exist with Zephyr's logs.
Ideally, the community would likely benefit from both solutions being implemented, allowing individual users the choice.
Estimate: 2-8 weeks, depending on options chosen.
Currently, it is not possible to enable CONFIG_USERSPACE while also supporting Rust. This is
mostly because the Rust code makes fairly extensive use of atomics and critical sections to maintain
safety. It is arguable that the Zephyr Userspace feature is somewhat redundant with the memory
protection provided by using Rust. However, it can also make sense to use userspace to protect a
program partially written in rust from parts of the code written in other languages.
There are two approaches here, with different degrees of difficulty:
Basic: Allow CONFIG_USERSPACE to be defined, but require all threads running Rust code to be
in system mode. Implementing this is a matter of documentation, and removing the current
build-time check against CONFIG_USERSPACE being defined.
Full: To allow Rust code to run in userspace, several things will have to be done:
Estimate: 1 week for basic, 4-8 weeks for Full.
Although the current async support is fully featured, despite the simplicity of the implementation, there is a bit of a barrier between async code, and the various Zephyr primitives. As there are several readily-available synchronization crates available that work well from async mode, this is sufficient for code that is completely written in Rust.
However, many interfaces in Zephyr use Zephyr primitives for synchronization. Specifically, semaphores are used fairly pervasively. In addition, using callbacks is not possible with userspace, so it is desirable to be able to use these other mechanisms.
A small bit of background as to how async works in Rust will be helpful. Any time the async
keyword is used in Rust, the compiler converts this to a block of code that returns a (hidden
definition) structure that implements
Future. This trait has requires two
things, one is an Output type which defines the final return type when the future has completed,
and the other is a poll function. The poll function runs the async code until it reaches a
blocking point, or finishes. It returns a value indicating which is the case. The implementation
is responsible for using the Context parameter passed to the future, specifically, it's Waker,
and to arrange for that Waker to be called when the Future is able to run again.
There are several possibilities for how to implement an async-able Semaphore.
I recommend starting with the triggered workqueue approach, as this has the least impact on both other async code, and the core of Zephyr. If/when Rust on Zephyr becomes more common, arguments in favor of adding deeper support will be easier to make.
Estimate: 3 weeks for triggered work, 5 weeks for adding support to callbacks (not counting time getting the RFC through).
The embassy-executor implementation for some architectures includes an executor that is capable of running async tasks from interrupt context. This is generally its own interrupt (e.g. most Cortex-M implementations have specific interrupts designed for software use). Implementing this in Zephyr requires registering an interrupt handler, and implementing a Zephyr-specific executor to run in this context.
This executor will allow for certain high priority operations to "await", being resumed in the high priority context.
It is unclear if this has benefits over just running an executor in a high priority thread. The irq handler will have overhead beyond bare-metal because the IRQ handlers need to be able to work with the Zephyr scheduler.
Estimate: 4 weeks
Currently, the devicetree support is driven by parsing the generated zephyr.dts file, combined
with a rust-specific config file that specifies the Rust device wraper types associated with various
nodes in the tree.
Ideally, this information would actually be part of the devicetree schema in Zephyr. There may also be improvements to use, not the generated dts file, but the data structures built by the edtlib parser that Zephyr uses to parse the device tree.
Some parts of this task:
Much will be driven by the needs of specific drivers as they are implemented.
Estimate: 2 weeks for using edtlib tree. Undetermined for additional work.
Adding device support for Zephyr involves the following:
For some devices, it might make sense to add Zephyr rtio support to that device, so that the existing RTIO support can be used to leverage adding async operations to that driver.
Some areas this might make sense to do:
A more detailed breakdown can be determined on a per-device basis, generally requiring the device API analisys to be done first.
Estimate: 1 week or less to analyze an API per device, and provide a more detailed estimate.
Zephyr includes numerous subsystems that would be useful from within Rust. Some of these are likely to be better served by just using existing Rust implementations (e.g. http, json, cbor). Others, however, are tightly coupled with Zephyr's drivers, and would work best as a direct interface.
Zephyr's network stack tries to present a posix-inspired interface to networking. At the time of writing, there is a draft PR that adds some initial support for interfacing directly to this.
This interface is largely blocking. There is a poll API in Zephyr, and using this for async would
be similar to what is needed to add async support to Semaphores. However, triggered work does not
currently support polling sockets, so this would need to add either a separate thread, or could dive
under the hood a bit, and leverage the semaphores used by the Zephyr networking stack.
Estimate: 6-12 weeks
Zephyr's USB subsystem is actually several different subsystems.
host: Not yet analyzedusb_c: Not yet analyzeddevice: This is a deprecated subsystem, and new work should be done with device_next.device_next: This is the new generation of device modeling. Instead of a fixed configuration,
where the various classes manipulate the descriptors themselves, the new generation requires the
user to provide the descriptors directly. This is generally done with a large number of C macros,
which would not be usable from Rust. However, there are a series of Rust crates around Rust
support for usb devices, such as usbd-hid, usbd-serial, etc, as well as usb-device that
likely could provide equivalent functionality. The work would then involved wrapping the
device_next apis as well as the apis for the various subsystems.The USB interface APIs work primarily around callbacks, which would work well with async in Rust.
Estimate: 4-8 weeks for device_next. Unknown for others.
Analysis: TBD
Note that there are numerous crates that implement cryptography in Rust, which would be available, likely unmodified in Rust code. Interfacing to Cryptography, or TEE provider code would be needed to make use of things like hardware accelerated crypto.
There are various other subsystems that would needed to be analyzed further to understand the work involved.
There are some external projects, especially in the Rust embedded community that would be useful to those using Rust on Zephyr.
The embassy project has developed a bootloader, currently with functionality similar to a subset of MCUboot. This project may be of interest notably because it is entirely written in Rust, and might help with analysis of safety and security of the code.
Beyond just initially making it work (it is bare-metal), there are some additional tasks that would increase its usefulness:
Estimate: 2 weeks to port to Zephyr. 4 weeks for extensive testing. 8 weeks for SUIT